﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace UndoRedo
{
    /// <summary>
    /// Special data structure for representing the data needed for undo/redo. It is memory efficient in that it uses only one array for the undo/redo information and that array is not resized during normal use as a regular stack would be.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CircularUndoRedoStack<T>
    {
        private readonly int _maxSize;
        private T[] _values;
        private int _index;
        private int _totalUndoableChangesOnStack;

        /// <summary>
        /// Maximum number of undoable changes that are supported.
        /// </summary>
        public int MaxSize { get { return this._maxSize; } }

        /// <summary>
        /// The number of undo operations that can be performed at this time.
        /// </summary>
        public int UndoableChangesCount { get { return (this._totalUndoableChangesOnStack - this.RedoableChangesCount); } }

        /// <summary>
        /// The number of redo operations that can be performed at this time.
        /// </summary>
        public int RedoableChangesCount { get; private set; }

        public bool CanUndo { get { return (this.UndoableChangesCount > 0); } }
        public bool CanRedo { get { return (this.RedoableChangesCount > 0); } }

        /// <summary>
        /// Creates a new Redo/Undo stack.
        /// </summary>
        /// <param name="maxSize">The maximum number of undo operations that will be supported.</param>
        public CircularUndoRedoStack(int maxSize)
        {
            if (maxSize <= 0)
                throw new ArgumentException(string.Format("The size of a \"{0}\" must be positive.", this.GetType().ToString()));

            this._maxSize = maxSize;
            this._values = new T[this._maxSize];
        }

        /// <summary>
        /// Adds a new undoable operation to the stack.
        /// </summary>
        /// <param name="value"></param>
        public void PushNewChange(T value)
        {
            this._values[this._index] = value;
            this.MoveToNextWriteIndex();

            //Update the number of undoable changes
            this._totalUndoableChangesOnStack = Math.Min(this._maxSize, this._totalUndoableChangesOnStack + 1);

            //Clear out any redoable changes
            this.RedoableChangesCount = 0;
        }

        /// <summary>
        /// Updates the current position within the stack and returns the most recent undoable change.
        /// </summary>
        /// <returns></returns>
        public T Undo()
        {
            //Make sure that there is a change to undo
            if (!this.CanUndo)
                throw new InvalidOperationException("Can not undo a change because there are no changes left to undo.");

            this.MoveToNextReadIndex();
            this.RedoableChangesCount++;

            return this._values[this._index];
        }

        /// <summary>
        /// Updates the current position within the stack and returns the most recent redoable change.
        /// </summary>
        /// <returns></returns>
        public T Redo()
        {
            if (!this.CanRedo)
                throw new InvalidOperationException("Can not redo an undone change since there are no undoable changes remaining.");

            this.RedoableChangesCount--;

            T changeToRedo = this._values[this._index];
            this.MoveToNextWriteIndex();

            return changeToRedo;
        }

        /// <summary>
        /// Moves the internal index ahead one either for a redo or else for adding a new change when there is an empty number of redos available.
        /// </summary>
        private void MoveToNextWriteIndex()
        { this._index = (this._index + 1) % this._maxSize; }

        /// <summary>
        /// Moves the internal index back one to the next undoable change.
        /// </summary>
        private void MoveToNextReadIndex()
        { this._index = (this._index + this._maxSize - 1) % this._maxSize; }
    }
}